{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "Zmw-nFX6Q_T7"
   },
   "source": [
    "# Project CodeNet Language Classification\n",
    "\n",
    "> Copyright (c) 2021 International Business Machines Corporation  \n",
    "Prepared by [Geert Janssen](geert@us.ibm.com>)\n",
    "\n",
    "## Introduction\n",
    "\n",
    "This notebook takes you through the steps of a simple experiment that shows\n",
    "how to create and exercise a Keras model to detect the language of a piece of\n",
    "source code. We will be using TensorFlow as its backend.\n",
    "For convenience, all the necessary steps will be in this single notebook.\n",
    "One might want to factor out some parts and put them in separate Python files\n",
    "for training, testing and inference.\n",
    "\n",
    "Let's start with a few obligatory imports:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "id": "AdZRI-dTRLhk"
   },
   "outputs": [],
   "source": [
    "import os\n",
    "import numpy as np\n",
    "import re\n",
    "import requests\n",
    "import tarfile\n",
    "import shutil"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "jJ6feP0BRQdO"
   },
   "source": [
    "## Approach\n",
    "\n",
    "Classification means determining the class a given sample belongs to. In our\n",
    "case we want to determine the programming language in which a given piece of code\n",
    "is written. There are many different ways of solving this problem. Here we\n",
    "choose a neural network approach where we train a convolutional model to do\n",
    "our bidding. The input will be a sample of source code and the output is an\n",
    "indication of its language.\n",
    "\n",
    "We will treat the input at the character level. We will assume that the input\n",
    "source file is mostly ASCII characters and the input to the neural network is\n",
    "an encoding of a small, fixed set of characters."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "-nrFdl8GRZZs",
    "outputId": "b213803b-9493-4e53-d591-ad572eb934a4"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "alphabet size: 68\n",
      "one-hot encoding for character c: [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]\n"
     ]
    }
   ],
   "source": [
    "letter = 'abcdefghijklmnopqrstuvwxyz'\n",
    "digits = '0123456789'\n",
    "others = '!\"#$%&\\'()*+,-./:;<=>?@[\\\\]^_`{|}~'\n",
    "alphabet = letter + digits + others\n",
    "print('alphabet size:', len(alphabet))\n",
    "\n",
    "# all-zeroes padding vector:\n",
    "pad_vector = [0 for x in alphabet]\n",
    "\n",
    "# pre-calculated one-hot vectors:\n",
    "supported_chars_map = {}\n",
    "\n",
    "for i, ch in enumerate(alphabet):\n",
    "  vec = [0 for x in alphabet]\n",
    "  vec[i] = 1\n",
    "  supported_chars_map[ch] = vec\n",
    "\n",
    "print('one-hot encoding for character c:', supported_chars_map['c'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "V3CtDPm-Rt8y"
   },
   "source": [
    "## Data\n",
    "\n",
    "For simplicity and to keep the (training) run-time within reasonable bounds, we\n",
    "use a selection of source files from Project CodeNet written in 10 different\n",
    "languages. For each language we select a 100 files of a size of at least 1000\n",
    "bytes but no larger than 5000 bytes. For each language, 10 files out the 100\n",
    "are set aside for testing the model.\n",
    "\n",
    "### Download and extract\n",
    "\n",
    "\n",
    "The data is available as a gzipped tar file `Project_CodeNet_LangClass.tgz`.\n",
    "The directory structure is `data/train/<language>/<source>` and\n",
    "`data/test/<language>/<source>`. Let's download the data and store it on the\n",
    "local filesystem:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "file_name = \"Project_CodeNet_LangClass.tar.gz\"\n",
    "data_url = f\"https://dax-cdn.cdn.appdomain.cloud/dax-project-codenet/1.0.0/{file_name}\"\n",
    "\n",
    "# Download tar archive to local disk\n",
    "with open(file_name, \"wb\") as f:\n",
    "    f.write(requests.get(data_url).content)\n",
    "    \n",
    "# Extract contents of archive to local disk\n",
    "if os.path.exists(\"data\"):\n",
    "    shutil.rmtree(\"data\")    \n",
    "with tarfile.open(file_name) as tf:\n",
    "    tf.extractall()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "jyA0CQwaSZlB",
    "outputId": "c83ad084-4c22-41b2-82bb-3fb9d0cc3722"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "data:\n",
      "\u001b[34mtest\u001b[m\u001b[m  \u001b[34mtrain\u001b[m\u001b[m\n",
      "\n",
      "data/train:\n",
      "\u001b[34mC\u001b[m\u001b[m          \u001b[34mC++\u001b[m\u001b[m        \u001b[34mHaskell\u001b[m\u001b[m    \u001b[34mJavaScript\u001b[m\u001b[m \u001b[34mPython\u001b[m\u001b[m\n",
      "\u001b[34mC#\u001b[m\u001b[m         \u001b[34mD\u001b[m\u001b[m          \u001b[34mJava\u001b[m\u001b[m       \u001b[34mPHP\u001b[m\u001b[m        \u001b[34mRust\u001b[m\u001b[m\n"
     ]
    }
   ],
   "source": [
    "!ls data data/train"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "CEwZLbmoqIHJ"
   },
   "source": [
    "The 10 programming languages are C, C#, C++, D, Haskell, Java, JavaScript, PHP,\n",
    "Python, and Rust. Note that some languages belong to the same family: C, C++,\n",
    "and D are very close in lexical elements and syntax."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "id": "SV4SN7rbqNJn"
   },
   "outputs": [],
   "source": [
    "langs = [\n",
    "  \"C\",\n",
    "  \"C#\",\n",
    "  \"C++\",\n",
    "  \"D\",\n",
    "  \"Haskell\",\n",
    "  \"Java\",\n",
    "  \"JavaScript\",\n",
    "  \"PHP\",\n",
    "  \"Python\",\n",
    "  \"Rust\"\n",
    "]\n",
    "\n",
    "num_classes = len(langs)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "RkC-R_EhiUy6"
   },
   "source": [
    "### Read a file and create samples\n",
    "\n",
    "Since the source files are of varying size and we want fixed length samples,\n",
    "simply truncating each file might bias the training to artifacts that\n",
    "typically only appear at the beginning of a file, such as copyrights statements\n",
    "and documentation or class and type definitions. We would probably get more\n",
    "interesting samples when we chunk each file. Here we split each file in 3\n",
    "parts based on the number of lines. Only then the parts will be truncated or\n",
    "padded as needed. This means that the number of samples is no longer equal to\n",
    "the number of files but up to 3 times as large."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "id": "u_jqGu9kijOD"
   },
   "outputs": [],
   "source": [
    "def get_source_snippets(file_name, breakup=True):\n",
    "  # Read the file content and lower-case:                                    \n",
    "  text = \"\"\n",
    "  with open(file_name, mode='r') as file:\n",
    "    text = file.read().lower()\n",
    "  lines = text.split('\\n')\n",
    "  nlines = len(lines)\n",
    "  if breakup and nlines > 50:\n",
    "    aThird = nlines//3\n",
    "    twoThirds = 2*aThird\n",
    "    text1 = '\\n'.join(lines[:aThird])\n",
    "    text2 = '\\n'.join(lines[aThird:twoThirds])\n",
    "    text3 = '\\n'.join(lines[twoThirds:])\n",
    "    return [text1, text2, text3]\n",
    "  return [text]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "JfhfLArikc26"
   },
   "source": [
    "To see the effect of `get_source_snippets` let's run it on a single train file:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "JuxCC8LOkykd",
    "outputId": "2556ccc9-1579-4b7e-a077-e84226935ae4"
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['import control.applicative\\nimport control.monad\\nimport control.monad.st\\n-- import qualified data.bytestring as bs\\nimport data.functor\\nimport data.function\\nimport data.monoid\\nimport data.maybe\\nimport data.list\\nimport qualified data.foldable as foldable\\nimport qualified data.set as set\\n--import qualified data.sequence as sequence\\nimport data.list.split\\nimport data.bits\\nimport data.char\\nimport data.ix\\nimport data.ratio\\nimport data.ord\\nimport data.tuple\\nimport data.array\\n--import data.array.unboxed\\n--import data.array.iarray\\nimport data.array.marray\\nimport data.array.io\\nimport data.array.st\\nimport data.ioref\\nimport data.stref\\nimport text.printf\\nimport ghc.st',\n",
       " \"-- import system.io.unsafe\\n \\n-- templete\\nreadint = read :: string -> int\\nreadinteger = read :: string -> integer\\nreaddouble = read :: string -> double\\ngetint = readln :: io int\\ngetints = map readint . words <$> getline\\ngetinteger = readln :: io integer\\ngetintegers = map readinteger . words <$> getline\\ngetdouble = readln :: io double\\ngetdoubles = map readdouble . words <$> getline\\nsjoin :: (show a) => [a] -> string\\nsjoin = unwords . map show\\ntjoin :: (show a, show b) => (a, b) -> string\\ntjoin (x, y) = show x ++ (' ' : show y)\\ncond :: a -> a -> bool -> a\\ncond t f c = if c then t else f\\napply2 :: (a -> a -> b) -> [a] -> b\\napply2 f [x,y] = f x y\\napply3 :: (a -> a -> a -> b) -> [a] -> b\\napply3 f [x,y,z] = f x y z\\napply4 :: (a -> a -> a -> a -> b) -> [a] -> b\\napply4 f [x,y,z,w] = f x y z w\\nfntuple :: (a -> b, a -> c) -> a -> (b, c)\\nfntuple (f,g) a = (f a, g a)\\nreplace :: (eq a) => a -> a -> [a] -> [a]\\nreplace x y = map (\\\\z -> if z==x then y else z)\\nbinmap :: (a -> a -> b) -> [a] -> [b]\",\n",
       " 'binmap f (x:xs@(y:_)) = f x y : binmap f xs\\nbinmap _ _ = []\\nsplitrec :: int -> [a] -> [[a]]\\nsplitrec _ [] = []\\nsplitrec n xs = let (y,ys) = splitat n xs in y : splitrec n ys\\ninfixl 7 `divceil`\\ndivceil :: integral a => a -> a -> a\\nx `divceil` y = (x+y-1) `div` y\\ncoverc :: ord a => (a, a) -> a -> bool\\ncoverc (l,r) x = l<=x && x<=r\\ncoverh :: ord a => (a, a) -> a -> bool\\ncoverh (l,r) x = l<=x && x<r\\nibsearch :: (int -> bool) -> (int,int) -> int\\nibsearch f (ok,ng) = if abs (ok-ng) <= 1 then ok else let mid = (ok + ng) `div` 2 in ibsearch f (if f mid then (mid,ng) else (ok,mid))\\nwhenm :: monad m => m bool -> m () -> m ()\\nwhenm c a = c >>= flip when a\\nunconsu :: [a] -> (a, [a])\\nunconsu (x:xs) = (x, xs)\\n-- templete\\n\\nlinev (x1,y1) (x2,y2) (x,y) = (y2-y1)*x - (x2-x1)*y - x1*y2 + x2*y1\\n\\nf p1 p2 p3 p = f1 p1 * f1 p > 0 && f2 p2 * f2 p > 0 && f3 p3 * f3 p > 0\\n  where\\n    f1 = linev p2 p3\\n    f2 = linev p3 p1\\n    f3 = linev p1 p2\\n\\nmain = map (map (apply2 (,)) . splitrec 2 . map readdouble . words) . lines <$> getcontents >>= mapm_ (putstrln . cond \"yes\" \"no\" . apply4 f)\\n    \\n']"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "get_source_snippets('data/train/Haskell/s084836192.hs', breakup=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "-ohVGLUWmHJN"
   },
   "source": [
    "### Prepare training data as numpy arrays\n",
    "\n",
    "Next we have to encode the snippets as one-hot vectors over the alphabet and\n",
    "represent them as `numpy` arrays. Ultimately,\n",
    "we do this for all files in a specified folder. But first the vectorization of a sample:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "id": "weska_xsusUu"
   },
   "outputs": [],
   "source": [
    "def turn_sample_to_vector(sample, sample_vectors_size=1024,\n",
    "                          normalize_whitespace=True):\n",
    "  if normalize_whitespace:\n",
    "    # Map (most) white-space to space and compact to single one:\n",
    "    sample = sample.replace('\\n', ' ').replace('\\r', ' ').replace('\\t', ' ')\n",
    "    sample = re.sub('\\s+', ' ', sample)\n",
    "\n",
    "  # Encode the characters to one-hot vectors:\n",
    "  sample_vectors = []\n",
    "  for ch in sample:\n",
    "    if ch in supported_chars_map:\n",
    "      sample_vectors.append(supported_chars_map[ch])\n",
    "\n",
    "  # Truncate to fixed length:\n",
    "  sample_vectors = sample_vectors[0:sample_vectors_size]\n",
    "\n",
    "  # Pad with 0 vectors:\n",
    "  if len(sample_vectors) < sample_vectors_size:\n",
    "    for i in range(0, sample_vectors_size - len(sample_vectors)):\n",
    "      sample_vectors.append(pad_vector)\n",
    "\n",
    "  return np.array(sample_vectors)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "Dy9_ReRuvkzK",
    "outputId": "5805b740-7b25-4400-898c-7cb1cea6ffb4"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Encoded sample shape: (10, 68)\n"
     ]
    }
   ],
   "source": [
    "sample = get_source_snippets('data/train/Haskell/s084836192.hs')[0]\n",
    "vec = turn_sample_to_vector(sample, sample_vectors_size=10)\n",
    "print('Encoded sample shape:', vec.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "Y1J7o6vTwqAk"
   },
   "source": [
    "Now do this for a whole file instead of a sample:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "id": "FXVbYeaQwtkl"
   },
   "outputs": [],
   "source": [
    "def turn_file_to_vectors(file_name, sample_vectors_size=1024,\n",
    "                         normalize_whitespace=True, breakup=True):\n",
    "  samples = get_source_snippets(file_name, breakup)\n",
    "  return [turn_sample_to_vector(s, sample_vectors_size, normalize_whitespace)\n",
    "          for s in samples]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "REi5x9-TxnJu"
   },
   "source": [
    "Lastly, pair the vectorized samples with their class labels:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "id": "zrz3sh2KxxkE"
   },
   "outputs": [],
   "source": [
    "def get_input_and_labels(root_folder, sample_vectors_size=1024, breakup=True):\n",
    "  X = []\n",
    "  Y = []\n",
    "  for i, lang in enumerate(langs):\n",
    "    print('Processing language:', lang)\n",
    "    # One-hot class label vector:\n",
    "    class_label = [0 for x in range(0, num_classes)]\n",
    "    class_label[i] = 1\n",
    "    # For all files in language folder:\n",
    "    folder = os.path.join(root_folder, lang)\n",
    "    for fn in os.listdir(folder):\n",
    "      if fn.startswith(\".\"):\n",
    "        continue  # Skip hidden files and Jupyterlab cache directories\n",
    "      file_name = os.path.join(folder, fn)\n",
    "      sample_vectors = turn_file_to_vectors(file_name,\n",
    "                                            sample_vectors_size=sample_vectors_size,\n",
    "                                            breakup=breakup)\n",
    "      for fv in sample_vectors:\n",
    "        X.append(fv)                 # the sample feature vector\n",
    "        Y.append(class_label)        # the class ground-truth\n",
    "\n",
    "  return np.array(X, dtype=np.int8), np.array(Y, dtype=np.int8)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "C2L6t9o_09q3"
   },
   "source": [
    "Now we can get all the training data in numpy arrays. For good measure we shuffle all the samples. Notice that the number of samples turns out to be 2098; starting from 900 source files it is obvious that not every file is split in 3."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "aPa5tyR10EVY",
    "outputId": "6e679775-04d3-4a22-e914-124c7af21044"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Processing language: C\n",
      "Processing language: C#\n",
      "Processing language: C++\n",
      "Processing language: D\n",
      "Processing language: Haskell\n",
      "Processing language: Java\n",
      "Processing language: JavaScript\n",
      "Processing language: PHP\n",
      "Processing language: Python\n",
      "Processing language: Rust\n",
      "samples shape (2098, 1024, 68)\n",
      "class labels shape: (2098, 10)\n"
     ]
    }
   ],
   "source": [
    "x, y = get_input_and_labels(root_folder='data/train')\n",
    "\n",
    "# Shuffle data\n",
    "shuffle_indices = np.random.permutation(np.arange(len(y)))\n",
    "x_shuffled = x[shuffle_indices]\n",
    "y_shuffled = y[shuffle_indices]\n",
    "\n",
    "print('samples shape', x_shuffled.shape)\n",
    "print('class labels shape:', y_shuffled.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "WzJqlvO42QYB"
   },
   "source": [
    "## Model\n",
    "\n",
    "A (batch of) encoded sample(s) is simultaneously input to 3 slightly different\n",
    "convolutional stages. Each stage consists of a 1-dimensional convolution\n",
    "followed by a 1-dimensional max pooling layer topped off by a flattening layer.\n",
    "The  kernel sizes of the stages are all different but the number of filters is\n",
    "the same. The 3 stages are concatenated followed by 2 dense layers with a\n",
    "dropout inbetween and lastly a softmax. The model summary does not show the nested model structure. `plot_model` however shows a nice graphical rendition of all the layers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 862
    },
    "id": "hnRwAi_K2Zuu",
    "outputId": "265caa05-f2c2-4d6a-98ba-dd426827b4ea"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Model: \"sequential\"\n",
      "_________________________________________________________________\n",
      "Layer (type)                 Output Shape              Param #   \n",
      "=================================================================\n",
      "model (Functional)           (None, 64512)             270208    \n",
      "_________________________________________________________________\n",
      "dense (Dense)                (None, 128)               8257664   \n",
      "_________________________________________________________________\n",
      "dropout (Dropout)            (None, 128)               0         \n",
      "_________________________________________________________________\n",
      "dense_1 (Dense)              (None, 10)                1290      \n",
      "=================================================================\n",
      "Total params: 8,529,162\n",
      "Trainable params: 8,529,162\n",
      "Non-trainable params: 0\n",
      "_________________________________________________________________\n"
     ]
    }
   ],
   "source": [
    "from tensorflow.keras.models import Sequential, Model\n",
    "from tensorflow.keras.layers import Activation, Dense, Dropout, Flatten, Input\n",
    "from tensorflow.keras.layers import Conv1D, MaxPooling1D, Concatenate\n",
    "\n",
    "# Model Hyperparameters\n",
    "kernel_sizes = (3, 9, 19)\n",
    "pooling_sizes = (3, 9, 19)\n",
    "num_filters = 128\n",
    "dropout_prob = 0.5\n",
    "hidden_dims = 128\n",
    "\n",
    "stage_in = Input(shape=(1024, 68))\n",
    "convs = []\n",
    "for i in range(0, len(kernel_sizes)):\n",
    "  conv = Conv1D(filters=num_filters,\n",
    "                kernel_size=kernel_sizes[i],\n",
    "                padding='valid',\n",
    "                activation='relu',\n",
    "                strides=1)(stage_in)\n",
    "  pool = MaxPooling1D(pool_size=pooling_sizes[i])(conv)\n",
    "  flatten = Flatten()(pool)\n",
    "  convs.append(flatten)\n",
    "\n",
    "if len(kernel_sizes) > 1:\n",
    "    out = Concatenate()(convs)\n",
    "else:\n",
    "    out = convs[0]\n",
    "\n",
    "stages = Model(inputs=stage_in, outputs=out)\n",
    "\n",
    "model = Sequential([\n",
    "    stages,\n",
    "    Dense(hidden_dims, activation='relu'),\n",
    "    Dropout(dropout_prob),\n",
    "    Dense(num_classes, activation='softmax')\n",
    "])\n",
    "\n",
    "model.summary()\n",
    "\n",
    "# Note: also need pydot and GraphViz installed for this.\n",
    "#from tensorflow.keras.utils import plot_model                               \n",
    "#plot_model(model, show_shapes=True, expand_nested=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "cBSSZjn_CV9p"
   },
   "source": [
    "## Training\n",
    "\n",
    "The 2098 samples with their ground truth (the class labels, i.e., the 10\n",
    "languages) are fed into the model. This constitutes one so-called epoch. To\n",
    "achieve a reasonable accuracy we have to train for several epochs. To speed\n",
    "things up, the samples are bundled in batches.\n",
    "Of the samples used in training, 10% are reserved for the validation step\n",
    "after each epoch. In our case that leaves 1888 for training and given a batch\n",
    "size of 64, one epoch then comprises some 30 batches.\n",
    "First the model is compiled and then we train it. If possible you should execute the `fit` call using a GPU. Otherwise, sit back and get yourself a cup of coffee."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "NCvEE3z5CenC",
    "outputId": "b3c6055d-ac84-445d-a233-02eef4a69366"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 1/20\n",
      "30/30 [==============================] - 9s 272ms/step - loss: 3.2192 - accuracy: 0.1520 - val_loss: 2.1471 - val_accuracy: 0.2000\n",
      "Epoch 2/20\n",
      "30/30 [==============================] - 8s 250ms/step - loss: 2.0165 - accuracy: 0.2636 - val_loss: 1.9698 - val_accuracy: 0.3524\n",
      "Epoch 3/20\n",
      "30/30 [==============================] - 8s 254ms/step - loss: 1.8209 - accuracy: 0.3363 - val_loss: 1.4646 - val_accuracy: 0.6667\n",
      "Epoch 4/20\n",
      "30/30 [==============================] - 7s 250ms/step - loss: 1.3232 - accuracy: 0.5277 - val_loss: 0.8675 - val_accuracy: 0.8571\n",
      "Epoch 5/20\n",
      "30/30 [==============================] - 8s 252ms/step - loss: 0.9016 - accuracy: 0.6876 - val_loss: 0.5528 - val_accuracy: 0.8905\n",
      "Epoch 6/20\n",
      "30/30 [==============================] - 8s 250ms/step - loss: 0.6413 - accuracy: 0.7662 - val_loss: 0.3709 - val_accuracy: 0.9238\n",
      "Epoch 7/20\n",
      "30/30 [==============================] - 8s 254ms/step - loss: 0.4523 - accuracy: 0.8421 - val_loss: 0.2870 - val_accuracy: 0.9238\n",
      "Epoch 8/20\n",
      "30/30 [==============================] - 8s 259ms/step - loss: 0.3710 - accuracy: 0.8692 - val_loss: 0.2407 - val_accuracy: 0.9429\n",
      "Epoch 9/20\n",
      "30/30 [==============================] - 9s 289ms/step - loss: 0.3236 - accuracy: 0.8884 - val_loss: 0.2072 - val_accuracy: 0.9571\n",
      "Epoch 10/20\n",
      "30/30 [==============================] - 9s 308ms/step - loss: 0.2539 - accuracy: 0.9126 - val_loss: 0.2160 - val_accuracy: 0.9476\n",
      "Epoch 11/20\n",
      "30/30 [==============================] - 10s 322ms/step - loss: 0.2680 - accuracy: 0.9072 - val_loss: 0.2398 - val_accuracy: 0.9333\n",
      "Epoch 12/20\n",
      "30/30 [==============================] - 10s 337ms/step - loss: 0.2116 - accuracy: 0.9225 - val_loss: 0.2333 - val_accuracy: 0.9333\n",
      "Epoch 13/20\n",
      "30/30 [==============================] - 10s 320ms/step - loss: 0.1944 - accuracy: 0.9304 - val_loss: 0.2132 - val_accuracy: 0.9429\n",
      "Epoch 14/20\n",
      "30/30 [==============================] - 10s 334ms/step - loss: 0.1740 - accuracy: 0.9389 - val_loss: 0.2233 - val_accuracy: 0.9429\n",
      "Epoch 15/20\n",
      "30/30 [==============================] - 10s 332ms/step - loss: 0.1595 - accuracy: 0.9379 - val_loss: 0.2042 - val_accuracy: 0.9429\n",
      "Epoch 16/20\n",
      "30/30 [==============================] - 9s 309ms/step - loss: 0.1662 - accuracy: 0.9403 - val_loss: 0.2442 - val_accuracy: 0.9381\n",
      "Epoch 17/20\n",
      "30/30 [==============================] - 9s 308ms/step - loss: 0.1565 - accuracy: 0.9419 - val_loss: 0.2397 - val_accuracy: 0.9476\n",
      "Epoch 18/20\n",
      "30/30 [==============================] - 9s 298ms/step - loss: 0.1699 - accuracy: 0.9251 - val_loss: 0.2369 - val_accuracy: 0.9476\n",
      "Epoch 19/20\n",
      "30/30 [==============================] - 9s 306ms/step - loss: 0.1643 - accuracy: 0.9327 - val_loss: 0.1967 - val_accuracy: 0.9524\n",
      "Epoch 20/20\n",
      "30/30 [==============================] - 9s 303ms/step - loss: 0.1650 - accuracy: 0.9276 - val_loss: 0.1971 - val_accuracy: 0.9429\n"
     ]
    }
   ],
   "source": [
    "batch_size = 64\n",
    "num_epochs = 20\n",
    "val_split = 0.1\n",
    "\n",
    "model.compile(loss='categorical_crossentropy', optimizer='adam',\n",
    "              metrics=['accuracy'])\n",
    "\n",
    "history = model.fit(x_shuffled, y_shuffled, batch_size=batch_size,\n",
    "                    epochs=num_epochs, validation_split=val_split,\n",
    "                    verbose=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "Nl_y6j_vEfK9"
   },
   "source": [
    "It is illustrative to chart the training process. The `history` contains all\n",
    "the information that we need."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 337
    },
    "id": "iBiHBmI-Ehr_",
    "outputId": "12a19474-7a78-45cb-e795-8d4b7428f0d7"
   },
   "outputs": [
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAsIAAAFACAYAAAC2ghqXAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/Z1A+gAAAACXBIWXMAAAsTAAALEwEAmpwYAACH/UlEQVR4nOzdd3xT1fvA8c9N0iTdEyh7I3u0RYZQwJapIg7g68CBigiKoKKighNFEBUVRRFBcPwQBXExylBGZVtEUPaUUaAtndn390cgUrrbpA3t8369+mqSe+69T9Lm9unJc85RVFVVEUIIIYQQoorRVHQAQgghhBBCVARJhIUQQgghRJUkibAQQgghhKiSJBEWQgghhBBVkiTCQgghhBCiSpJEWAghhBBCVEmSCHvYr7/+iqIonDhxokT7KYrCF1984aGoyk95PI8jR46gKAobNmwo0Xl79uzJgw8+WObzz5s3D51OV+bjCCEqD7n2y7XfndwVs8hLEuGLFEUp9KtBgwalOm7Xrl05deoUtWrVKtF+p06d4vbbby/VOYVnXr8TJ06gKAq//vprrseHDh3Kv//+69ZzCSHKh1z7Kxe59ouSkm6si06dOuW6nZiYyG233caOHTuoWbMmAFqtNld7i8WCXq8v8rh6vZ7IyMgSx1OafcR/yvP18/X1xdfXt9zO542sVis+Pj4VHYYQJSbX/spFrv2ipKRH+KLIyEjXV1hYGADVqlVzPVa9enXee+897rzzToKDgxk2bBgAzz//PC1atMDPz4+6desycuRILly44DrulR+PXbqfkJBAbGwsfn5+tGzZkmXLluWK58qPdxRF4cMPP2TYsGEEBgZSp04d3njjjVz7nD9/nsGDB+Pv70+NGjWYOHEi9957L/Hx8YU+96Kew6WPfzZu3EhUVBR+fn5ER0ezdevWXMdZu3Ytbdu2xWg00rZtW9auXVvoeffv34+iKCQmJuZ6fPPmzSiKwv79+wGYMWMG7du3JyAggMjISP73v//l+uOVnytfv6NHj9KvXz98fX2pW7cu77//fp59vvrqKzp16kRwcDARERHccMMN7Nu3z7W9bt26APTq1StXT1F+H4/98ssvREdHYzAYqF69OqNGjSIrK8u1/b777iM+Pp5PPvmE+vXrExQUxMCBAzlz5kyhz6uoGAGSk5O5//77qVGjBkajkWuuuYbPPvvMtf3gwYPcfvvthIWF4efnR9u2bfnpp58KfC5X9oZc+h3++eef6datG0ajkU8//ZTU1FTuvvtu6tWrh6+vL9dccw3Tp0/nysUrFy5cSHR0NEajkfDwcPr3709qairz5s0jJCSE7OzsXO1feeUVmjZtmuc4QriDXPvl2n81XPuvZLVaefbZZ6lduzZ6vZ6WLVvy1Vdf5Wrz6aef0qJFC4xGI2FhYcTGxrp+H9PT07n//vuJjIzEYDBQt25dnnjiiRLFUFlIIlwCL7/8Ml27dmXHjh289tprgPM/wk8++YQ9e/Ywb948fv31V8aMGVPksZ566imee+45du7cSadOnRg6dCipqalFnj82NpakpCQmTJjAc889x+rVq13b77//fnbu3MlPP/3EmjVrOHHiBN9//32RsRTnOTgcDiZMmMCMGTPYsWMH1atXZ8iQIdhsNgBOnjzJjTfeSHR0NDt27GD69Ok8/vjjhZ63adOmdOnShQULFuR6/PPPP6dLly40bdrU9dhbb73Frl27WLJkCceOHeN///tfkc/rElVVueWWWzh//jy//vorP/74Iz/88AM7duzI1c5sNvPCCy+wY8cOEhIS0Gq13HDDDVgsFgBX+++++45Tp07l+WNwyZ9//snAgQOJjY1l586dfP755/z000+MHDkyV7utW7eydu1afv75Z1asWMGuXbt46qmnCn0uRcWYk5NDjx492LlzJ19++SV79uzh/fffx8/PD4DTp0/TtWtX0tLS+OGHH9i1axevvvoqGk3JLwVPPvkkzzzzDH///Tc33XQTZrOZ1q1b8/3337Nnzx4mTpzIiy++yLx581z7zJ07l7vvvptBgwaxY8cO1q5dS79+/bDb7QwdOhRFUVi0aJGrvcPh4LPPPuPBBx9EUZQSxyiEO8i1X679ULHX/is999xzzJ49m3fffZe//vqLu+++m7vvvtv1e7F9+3ZGjhzJhAkT2Lt3L7/99hv33HOPa/9Lz3fp0qXs37+fhQsX0qJFixLFUGmoIo+1a9eqgHr8+HHXY4A6fPjwIvddvHixqtfrVbvdnu+xLt3/7rvvXPucPn1aBdTly5fnOt+CBQty3X/sscdynat58+bqs88+q6qqqu7bt08F1FWrVrm2WywWtU6dOmpcXFxJnn6e5zB37lwVULdv3+5qs2nTJhVQ//nnH1VVVfX5559X69Wrp1qtVlebH3/8Mc/zuNJHH32khoaGqmazWVVVVTWbzWpYWJg6a9asAvfZsWOHCqgnTpxQVVVVDx8+rALq+vXrXW0uP29CQoIKqHv37nVtT05OVo1Go/rAAw8UeJ7z58+rgLphwwZVVVX1+PHjKqCuXbs2V7u5c+eqWq3Wdf/uu+9WO3bsmKvN999/ryqKoh45ckRVVVW999571WrVqqkmk8nVZsqUKWpkZGSB8RQnxk8//VQ1GAy5fncv98ILL6g1atRQMzMz891+5XNR1bzP+9Lv8Pz584uMb8yYMWp8fLzrft26ddXRo0cX2P6xxx5Tr7vuOtf95cuXqz4+PuqZM2eKPJcQZSXXfrn2q6p3Xvt79OjhijkrK0vV6/XqzJkzc7UZNGiQ2qtXL1VVnT/LoKAg9cKFC/keb+DAgeq9995b6DmrCukRLoFrr702z2OLFy8mNjaWWrVqERAQwF133YXFYuH06dOFHqt9+/au2zVq1ECr1Rb50cjl+wDUqlXLtc+ePXsA6Ny5s2u7j48PMTExhR6zuM9BURTatWuX69xArvNfe+21uT4m6tatW5HnHjp0KNnZ2a6P5n/66SeysrIYOnSoq82vv/5K3759qVu3LoGBga7jHj16tMjjX4otIiKCZs2auR6rVq0a11xzTa52SUlJ3HLLLTRs2JDAwEDq1atXovNcsnv3bmJjY3M91qNHD1RVdf2cAJo3b47BYHDdv/znWZCiYty+fTstW7akTp06+e6/fft2unbtir+/f4meU36ufD84HA6mTJlC+/btiYiIICAggFmzZrliS05O5vjx4/Tp06fAYz788MNs3LiRv//+G4DZs2czcOBAqlevXuZ4hSgtufbLtb84PHntv9yBAwewWCz5nmv37t0A9O7dm0aNGtGwYUP+97//8cknn3Du3DlX21GjRvHtt9/SunVrHn/8cZYtW4bD4SjR860sJBEugSuTh82bNzN48GBiY2NZsmQJO3bsYNasWQCuj1QKkt9gi6J+Ca/cR1GUPPuU9OPj4j4HjUaTa9DIpfOU9Y0TGhrKTTfdxPz58wGYP38+AwcOJCQkBIBjx44xYMAAGjRowP/93/+xbds2fvjhhzzxlVV2djZ9+vRBURTmzp3Lli1b2Lp1K4qiuPU8l8vv56kWUgdbHjHmVyJhtVrzbXvl+2H69Om88cYbjBkzhoSEBJKSknjwwQdLFFurVq3o1q0bs2fPJjk5mR9++IERI0aU7EkI4WZy7ZdrvzuV9NpfGgEBAWzbto0lS5bQrFkzZs2aRZMmTdi+fTsAffv25dixYzz//POYTCbuvvturr/+eux2u1vjuBpIIlwGGzZsICIigtdee41OnTrRrFmzEs8Z6S4tW7YE4Pfff3c9ZrPZXL/0BXHXc2jZsiVbtmzJ9SbauHFjsfa99957+eWXX9i7dy+//PJLrjqmrVu3kpOTw7vvvst1113HNddcU+JBBS1btuTcuXOuARgA586dY+/eva77f//9N2fPnmXy5Mn07NmTFi1akJqamuvidOniVdSFolWrVqxbty7XY7/99huKotCqVasSxX654sQYHR3Nnj17CvwZRkdHk5iYmGvwxuWqV6+O3W7P9RpfWU9XkHXr1tGvXz+GDx9Ohw4daNKkSa7XvHr16tSpU4eVK1cWepyHH36Y+fPn88knn1C7dm169+5drPMLUV7k2p/7/HLtd/LUtf9KTZo0wWAw5Huu1q1bu+5rtVpiY2N55ZVX2L59OzVr1sw1oC4sLIw77riDjz/+mJ9//pnffvstV891VSGJcBlcc801nD17ljlz5nDo0CHmz5/Phx9+WCGxNG3alJtuuonRo0e7fpkffvhh0tPTC+0pcNdzeOSRRzh79iwjRozg77//ZvXq1Tz//PPF2rdfv36Ehobyv//9j9DQUPr165freSmKwvTp0zl8+DDff/89r7zySolii4uLo127dtx9991s2bKFpKQk7rrrrlzTfdWvXx+DwcD777/PwYMHWb16NY8//niu1+7Sx/0rV67k9OnTBQ5wGT9+PDt27GDcuHH8888/LF++nMcee4y77rrL9ZFbaRQnxjvuuIP69eszcOBAVq1axeHDh1m9ejULFy4EnB+HORwObr75ZjZu3Mjhw4f56aefXCPXr732WgIDA3n22WfZv38/y5cvL/brfc011/Drr7+ydu1a9u3bxwsvvMDmzZtztXnxxRf5+OOPefXVV/n777/ZvXs3H3zwQa6P7C7NAfrqq6/KIDnhleTa/x+59v/HU9f+K/n5+TFmzBgmTpzIokWL2LdvH6+//jpLly7lueeeA2Dp0qW88847bN++nWPHjvH9999z/Phx1z9Ozz//PIsXL2bv3r3s37+fL7/8koCAALfGebWQ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AMpcI7x+vYGQEAetW8vSREII4W2KUycMMHhwDhaLwg8/SI2bEFVBsWqEixIdHc11112Hj48PCQkJzJw5kxdffDFPu1WrVrFq1SoApkyZQkREhDtOXyba06dRfXzQNWpEaaNRVUhM9CEuzkH16u55Tjqdziten8t5W0wST9G8LSZviwe8Mybhfo6aNbE1aID+99/JGjGiwHZt2lhp3tzKokV+3HNPdjlGKISoCEUmwmFhYZw/f951//z5865BcZcEBga6bsfFxfHFF1/ke6z4+Hji4+Nd972h5zh092509eujliGeAwe0nDhRg8cey+DcOfdcOCMiIrzi9bmct8Uk8RTN22LytnigdDHVqlXLQ9EITzJ36YLvsmXgcIAm/w9EFQUGD87m1VeDOXBAS5Mm9nKOUghRnoosjWjcuDGnTp0iOTkZm81GYmIiMTExudqkpqa6bm/bti3PQDpvpjt8uMwryq1fL/XBQgjh7SxduqBJS0NXSJ0wOOuENRqVRYv8yikyIURFKbJHWKvVMnz4cCZPnozD4aBXr17UrVuXhQsX0rhxY2JiYli2bBnbtm1Dq9USEBDAqFGjyiP2snM40B05grlHD/RlOMz69Qbq17dRv770HAghhLe6VCds+P13bK1bF9iuRg0HPXua+e47P55+OqOgSSaEEJVAsWqEo6KiiIqKyvXY0KFDXbfvvPNO7rzzTvdGVg60p06hmEzYGjYsdSJss0FiooGBA3PcGpsQQgj3ctSuja1+ffSbNpH10EOFth08OJtHHglj40Y9sbGySJIQlVWVXllOe/AgALZGjUp9jKQkHzIyNFIWIYQQVwFL584YNm1y1gkXok8fE8HBDimPEKKSq9KJ8KU5hG1lqBFev96Aoqhcd50kwkII4e3Ml+qE//mn0HZGI9x0Uw6//GIkI0PmFBaisqraifChQzh8fXFERpb6GBs2GGjTxkpYmOrGyIQQQniCpUsXwFknXJQhQ7IxmTT8/LPR02EJISpI1U6EL80YUcA0OkXJylLYvl0vZRFCCHGVsNepg61uXfSbNhXZNirKSqNGNimPEKISq9qJ8KFDZSqL2LRJj9Wq0K2bJMJCCHG1sHTp4lxhrog64UtzCm/aZODoUZk6QojKqOomwlYr2uPHyzRQbv16A0ajyrXXyohiIYS4Wpg7d0abmopu374i2952WzaKovLt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\n",
      "text/plain": [
       "<Figure size 864x360 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "plt.style.use('ggplot')\n",
    "\n",
    "def plot_history(history):\n",
    "  acc = history.history['accuracy']\n",
    "  val_acc = history.history['val_accuracy']\n",
    "  loss = history.history['loss']\n",
    "  val_loss = history.history['val_loss']\n",
    "  x = range(1, len(acc) + 1)\n",
    "\n",
    "  plt.figure(figsize=(12, 5))\n",
    "  plt.subplot(1, 2, 1)\n",
    "  plt.plot(x, acc, 'b', label='Training acc')\n",
    "  plt.plot(x, val_acc, 'r', label='Validation acc')\n",
    "  plt.title('Training and validation accuracy')\n",
    "  plt.legend()\n",
    "  plt.subplot(1, 2, 2)\n",
    "  plt.plot(x, loss, 'b', label='Training loss')\n",
    "  plt.plot(x, val_loss, 'r', label='Validation loss')\n",
    "  plt.title('Training and validation loss')\n",
    "  plt.legend()\n",
    "  plt.show()\n",
    "\n",
    "plot_history(history)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "0OJVXeXwIb0U"
   },
   "source": [
    "## Test\n",
    "\n",
    "Finally, let's exercise our mint new model and see how it performs on a set of\n",
    "samples hitherto unseen, in other words predict the language of the test set.\n",
    "We re-use the `get_input_and_labels` and have it process all files in the\n",
    "`test` directory. We predict the outcome of each sample and compare it against\n",
    "the expected label (language)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "gQawmBrRIjG8",
    "outputId": "1fa488a0-0fa3-4609-b8b1-c7985f858d00"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Processing language: C\n",
      "Processing language: C#\n",
      "Processing language: C++\n",
      "Processing language: D\n",
      "Processing language: Haskell\n",
      "Processing language: Java\n",
      "Processing language: JavaScript\n",
      "Processing language: PHP\n",
      "Processing language: Python\n",
      "Processing language: Rust\n",
      "shape of test samples (100, 1024, 68)\n"
     ]
    }
   ],
   "source": [
    "x, y = get_input_and_labels(root_folder='data/test', breakup=False)\n",
    "print('shape of test samples', x.shape)\n",
    "y_hat = model.predict(x)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "Oyfbx8CMT7o_"
   },
   "source": [
    "Here is a very simple evaluation of the model. For each language we count the number of correct prediction, i.e., the softmax output with the highest probability (viz. `argmax`):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "jF5sKu52PhxD",
    "outputId": "99943b29-ab28-461b-edfc-2d03057f9d25"
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "C         :   9 correct\n",
      "C#        :  10 correct\n",
      "C++       :   9 correct\n",
      "D         :  10 correct\n",
      "Haskell   :  10 correct\n",
      "Java      :  10 correct\n",
      "JavaScript:  10 correct\n",
      "PHP       :  10 correct\n",
      "Python    :  10 correct\n",
      "Rust      :  10 correct\n"
     ]
    }
   ],
   "source": [
    "hits = {}\n",
    "for lang in langs:\n",
    "  hits[lang] = 0\n",
    "\n",
    "for i in range(len(x)):\n",
    "  expected_lang  = langs[np.argmax(y[i], axis=0)]\n",
    "  predicted_lang = langs[np.argmax(y_hat[i], axis=0)]\n",
    "  if predicted_lang == expected_lang:\n",
    "    hits[expected_lang] += 1\n",
    "\n",
    "for lang in langs:\n",
    "  print(\"%-10s: %3d correct\" % (lang, hits[lang])) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "NwQQbaIgUm0c"
   },
   "source": [
    "### Home work\n",
    "\n",
    "Find out which test files are incorrectly predicted, and as what? Also, do a more thorough analysis and print the precision, recall, and F1 score of each class."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "id": "tCTepYLlnQF1"
   },
   "source": [
    "## Acknowledgement\n",
    "\n",
    "The idea and much of the Python source code for this notebook was derived from the github https://github.com/aliostad/deep-learning-lang-detection by Ali Kheyrollahi."
   ]
  }
 ],
 "metadata": {
  "accelerator": "GPU",
  "colab": {
   "collapsed_sections": [],
   "name": "Project_CodeNet_LangClass.ipynb",
   "provenance": []
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
